package core

import (
	"fmt"
	"math/rand"
	"sync"
	"zinx/mmo_game/pb"
	"zinx/ziface"
	"github.com/golang/protobuf/proto"
)

//玩家对象
type Player struct {
	Pid  int32              //玩家ID
	Conn ziface.IConnection //当前玩家的连接
	X    float32            //平面x坐标
	Y    float32            //高度
	Z    float32            //平面y坐标 (注意不是Y)     客户端那边是这么定义的
	V    float32            //旋转0-360度
}

/*
	Player ID 生成器
*/
var PidGen int32 = 1  //用来生成玩家ID的计数器
var IdLock sync.Mutex //保护PidGen的互斥机制

//创建一个玩家对象
func NewPlayer(conn ziface.IConnection) *Player {
	//生成一个PID
	IdLock.Lock() //这里要加锁
	id := PidGen
	PidGen++
	IdLock.Unlock()

	p := &Player{ //Plyaer类中有当前玩家的ID，和当前玩家与客户端绑定的conn，还有就是地图的坐标信,NewPlayer()提供初始化玩家方法。
		Pid:  id,
		Conn: conn,
		X:    float32(160 + rand.Intn(10)), //随机在160坐标点 基于X轴偏移若干坐标
		Y:    0,                            //高度为0
		Z:    float32(134 + rand.Intn(17)), //随机在134坐标点 基于Y轴偏移若干坐标
		V:    0,                            //角度为0，尚未实现
	}

	return p
}

/*
	发送消息给客户端，
	主要是将pb的protobuf数据序列化之后,再调用zinx的SendMsg方法
	这里要注意的是，SendMsg()是将发送的数据，通过proto序列化，然后再调用Zinx框架的SendMsg方法发送给对方客户端.
*/
func (p *Player) SendMsg(msgId uint32, data proto.Message) {
	fmt.Printf("before Marshal data = %+v\n", data) //Go语言中打印结构体推荐使用“%+v”，而不是“%v”。看一下二者区别：
	/*
	   type info struct {
	       name string
	       id int
	   }

	   func main()  {
	       v := info{"Nan", 33}
	       fmt.Printf("%v\n", v)
	       fmt.Printf("%+v\n", v)
	   }
	   运行结果如下：

	   {Nan 33}
	   {name:Nan id:33}

	*/

	//将proto Message结构体序列化
	msg, err := proto.Marshal(data)
	if err != nil {
		fmt.Println("marshal msg err: ", err)
		return
	}
	fmt.Printf("after Marshal data = %+v\n", msg)

	if p.Conn == nil { //先判断下链接是否存在
		fmt.Println("connection in player is nil")
		return
	}
	//调用Zinx框架的SendMsg发包
	if err := p.Conn.SendMsg(msgId, msg); err != nil {
		fmt.Println("Player SendMsg error !")
		return
	}

	return
}

/*
根据我们之前的流程分析，那么在客户端建立连接过来之后，
Server要自动的回复给客户端一个玩家ID，同时也要将当前玩家的坐标发送给客户端。
所以我们这里面给Player定制了两个方法Player.SyncPid()和Player.BroadCastStartPosition()
*/

//SyncPid()发送MsgID:1的消息，将当前上线的用户ID发送给客户端
//告知客户端pid,同步已经生成的玩家ID给客户端
func (p *Player) SyncPid() {
	//组建MsgId0 proto数据
	data := &pb.SyncPid{
		Pid: p.Pid,
	}

	//发送数据给客户端
	p.SendMsg(1, data) //这里的data就是玩家id
}

//广播玩家自己的出生地点
func (p *Player) BroadCastStartPosition() {

	msg := &pb.BroadCast{
		Pid: p.Pid,
		Tp:  2, //TP2 代表广播坐标
		Data: &pb.BroadCast_P{ //为什么是BroadCast_P？去msg.pb.go和msg.proto查看,代表BroadCast的position属性
			&pb.Position{
				X: p.X,
				Y: p.Y,
				Z: p.Z,
				V: p.V,
			},
		},
	}

	p.SendMsg(200, msg)
}


//广播玩家聊天
func (p *Player) Talk(content string) {
	//1. 组建MsgId200 proto数据
	msg := &pb.BroadCast{
		Pid:p.Pid,
		Tp:1,//TP 1 代表聊天广播
		Data: &pb.BroadCast_Content{
			Content: content,
		},
	}

	//2. 得到当前世界所有的在线玩家
	players := WorldMgrObj.GetAllPlayers()

	//3. 向所有的玩家发送MsgId:200消息
		//当迭代切片时，关键字 range 会返回两个值，第一个值是当前迭代到的索引位置，第二个值是该位置对应元素值的一份副本，
		for _, player := range players {
			player.SendMsg(200, msg)
	}
}
//这里的过程只有两个重要过程，一个是将自己的坐标信息发送给AOI范围周边的玩家，一个是将周边玩家的坐标信息发送给自己的客户端。
func(p *Player) SyncSurrounding(){
	//1 根据自己的位置，获取周围九宫格内的玩家pid
	pids := WorldMgrObj.AoiMgr.GetPIDsByPos(p.X,p.Z)   //pids是一个切片,里面存放PID（int类型）
	//2 根据pid得到所有玩家对象
	players := make([]*Player, 0, len(pids))
	//3 给这些玩家发送MsgID:200消息，让自己出现在对方视野中
	for _,pid := range pids{
		players=append(players,WorldMgrObj.GetPlayerByPid(int32(pid)))
	}

	//3.1 组建MsgId200 proto数据
	msg := &pb.BroadCast{
		Pid:p.Pid,
		Tp:2,//TP2 代表广播坐标
		Data: &pb.BroadCast_P{
			P:&pb.Position{
				X:p.X,
				Y:p.Y,
				Z:p.Z,
				V:p.V,
			},
		},
	}
	//3.2 每个玩家分别给对应的客户端发送200消息，显示人物
	for _, player := range players {
		player.SendMsg(200, msg)
	}
	//4 让周围九宫格内的玩家出现在自己的视野中
	//4.1 制作Message SyncPlayers 数据
/*
//玩家信息
message Player{
	int32 Pid=1;
	Position P=2;
}
*/	
	playersData := make([]*pb.Player, 0, len(players))
	for _, player := range players {
		p := &pb.Player{
			Pid:player.Pid,
			P:&pb.Position{
				X:player.X,
				Y:player.Y,
				Z:player.Z,
				V:player.V,
				},
			}
		playersData = append(playersData, p)   //将周围九宫格玩家位置信息发放在playersData 
	}
	//4.2 封装SyncPlayer protobuf数据
	SyncPlayersMsg := &pb.SyncPlayers{
		Ps:playersData[:],
	}
	//4.3 给当前玩家发送需要显示周围的全部玩家数据       要让其他人包括自己出现在自己的视野中
	p.SendMsg(202, SyncPlayersMsg)    
}

//广播玩家位置移动
func (p *Player) UpdatePos(x float32, y float32, z float32, v float32) {
	//更新玩家的位置信息  移动之后的位置在这里更新了！！！
	p.X = x
	p.Y = y
	p.Z = z
	p.V = v
	//组装protobuf协议，发送位置给周围玩家
	msg := &pb.BroadCast{
		Pid:p.Pid,
		Tp:4,     //4 - 移动之后的坐标信息
		Data: &pb.BroadCast_P{
			P:&pb.Position{
				X:p.X,
				Y:p.Y,
				Z:p.Z,
				V:p.V,
			},
		},
	}
	//获取当前玩家周边全部玩家
	players := p.GetSurroundingPlayers()
	//向周边的每个玩家发送MsgID:200消息，移动位置更新消息
	for _, player := range players {
		player.SendMsg(200, msg)
	}
}
/*
其中GetSurroundingPlayers()是获取当前玩家AOI周边的玩家Player对象有哪些。
该方法的整体思路是获取周边的所有玩家，发送位置更新信息。
*/
//获得当前玩家的AOI周边玩家信息
func (p *Player) GetSurroundingPlayers() []*Player {
	//得到当前AOI区域的所有pid
	pids := WorldMgrObj.AoiMgr.GetPIDsByPos(p.X, p.Z)
   //GetPIDsByPos得到的是包含pid的切片pids，但没有具体的玩家信息
   //所以再根据pid得到玩家信息    WorldMgrObj.GetPlayerByPid
	//将所有pid对应的Player放到Player切片中
	players := make([]*Player, 0, len(pids))
	for _, pid := range pids {
		players = append(players, WorldMgrObj.GetPlayerByPid(int32(pid)))
	}

	return players
}

//玩家下线
func (p *Player) LostConnection() {
	//1 获取周围AOI九宫格内的玩家
	players := p.GetSurroundingPlayers()

	//2 封装MsgID:201消息
	msg := &pb.SyncPid{
		Pid:p.Pid,
	}

	//3 向周围玩家发送消息
	for _, player := range players {
		player.SendMsg(201, msg)
	}

	//4 将当前玩家从AOI管理器、世界管理器中摘除
	WorldMgrObj.AoiMgr.RemoveFromGridByPos(int(p.Pid), p.X, p.Z)
	WorldMgrObj.RemovePlayerByPid(p.Pid)
}